package com.guchenbo.example.httpclient;

import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.UnsupportedSchemeException;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.routing.HttpRoutePlanner;
import org.apache.http.impl.conn.DefaultSchemePortResolver;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager;
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.apache.http.nio.conn.NoopIOSessionStrategy;
import org.apache.http.nio.conn.SchemeIOSessionStrategy;
import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy;
import org.apache.http.nio.reactor.ConnectingIOReactor;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLContext;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class CustomHttpAsyncClient {

    private final static Logger logger = LoggerFactory.getLogger(CustomHttpAsyncClient.class);

    public int CONNECT_TIMEOUT = 6; //unit: second
    public int READ_TIMEOUT = 30; //unit: second
    public int CONNECT_REQUEST_TIMEOUT = 6; //unit: second

    private static CloseableHttpAsyncClient asyncClient;

    private String proxyCache;
    private Map<String, String> proxyMap = new HashMap<>();
    private Map<String, Boolean> bypassAddresses = new HashMap<>();
    private Map<String, Boolean> bypassPatternAddresses = new HashMap<>();
    private int threadCount = Runtime.getRuntime().availableProcessors() * 2;

    private String userName;
    private String passWord;

    public CustomHttpAsyncClient(String bypass, int ioThread, int readTimeout, String userName, String passWord) {
        if (ioThread > 0)
            this.threadCount = ioThread;

        if (readTimeout > 30)
            this.READ_TIMEOUT = readTimeout;

        if (StringUtils.isNotEmpty(bypass)) {
            String[] strings = bypass.split(",");
            for (String s : strings) {
                if (s.contains("*")) {
                    String k = s.substring(0, s.indexOf("*"));
                    bypassPatternAddresses.put(k, true);
                } else {
                    bypassAddresses.put(s, true);
                }
            }
        }

        this.userName = userName;
        this.passWord = passWord;

        initHttpAsyncClient();
    }

    /**
     * initialize apache async http client
     */
    private void initHttpAsyncClient() {
        try {
            System.setProperty("jsse.enableSNIExtension", "false");

            RequestConfig requestConfig = RequestConfig.custom().setCircularRedirectsAllowed(false)
                            .setSocketTimeout(READ_TIMEOUT * 1000).setConnectTimeout(CONNECT_TIMEOUT * 1000)
                            .setConnectionRequestTimeout(CONNECT_REQUEST_TIMEOUT * 1000)
                            .setCookieSpec(CookieSpecs.IGNORE_COOKIES).setMaxRedirects(0).build();

            SSLContext sslcontext = SSLContexts.custom().useProtocol("TLS")
                            .loadTrustMaterial(null, (chain, authType) -> true).build();
            SSLIOSessionStrategy sslSessionStrategy = new SSLIOSessionStrategy(sslcontext, (s, sslSession) -> true);

            Registry<SchemeIOSessionStrategy> sessionStrategyRegistry = RegistryBuilder.<SchemeIOSessionStrategy>create()
                            .register("http", NoopIOSessionStrategy.INSTANCE).register("https", sslSessionStrategy)
                            .build();

            IOReactorConfig ioReactorConfig = IOReactorConfig.custom().setIoThreadCount(threadCount).build();
            ConnectingIOReactor ioReactor = new DefaultConnectingIOReactor(ioReactorConfig);
            PoolingNHttpClientConnectionManager cm = new PoolingNHttpClientConnectionManager(ioReactor,
                            sessionStrategyRegistry);
            cm.setDefaultMaxPerRoute(1000);
            cm.setMaxTotal(10000);

            HttpAsyncClientBuilder builder = HttpAsyncClients.custom().setConnectionManager(cm)
                            .setDefaultRequestConfig(requestConfig).setSSLStrategy(sslSessionStrategy)
                            .setSSLHostnameVerifier((s, sslSession) -> true);

            HttpRoutePlanner routePlanner = (host, request, context) -> {
                String hostname = host.getHostName();
                if (bypassAddresses.containsKey(hostname)) {
                    return new HttpRoute(host);
                    //return direct route
                } else if (bypassPatternAddresses.size() > 0) {
                    for (String key : bypassPatternAddresses.keySet()) {
                        if (hostname.startsWith(key)) {
                            return new HttpRoute(host);
                        }
                    }
                }

                String sproxy = "127.0.0.1-8888:YYS-000000,YYS-000001,YYS-000002,YYS-330000,YD-000009,YYS-130000,DX-420000,YD-330000";
                importProxy(sproxy);

                String taskId = request.getFirstHeader("PARAM-TASK-ID").getValue();
                String cType = request.getFirstHeader("PARAM-CHANNEL-TYPE").getValue();
                String cCode = request.getFirstHeader("PARAM-CHANNEL-CODE").getValue();
                if (StringUtils.isNotBlank(taskId) && StringUtils.isNotBlank(cType) && StringUtils.isNotBlank(cCode)) {
                    String thisK = cType + "-" + cCode;
                    if (proxyMap.containsKey(thisK)) {
                        String thisP = proxyMap.get(thisK);
                        String[] hp = thisP.split("-");
                        HttpHost proxyHost = new HttpHost(hp[0], Integer.parseInt(hp[1]));

                        final HttpHost target;
                        if (host.getPort() <= 0) {
                            try {
                                target = new HttpHost(host.getHostName(),
                                                DefaultSchemePortResolver.INSTANCE.resolve(host), host.getSchemeName());
                            } catch (final UnsupportedSchemeException ex) {
                                throw new HttpException(ex.getMessage());
                            }
                        } else {
                            target = host;
                        }
                        final boolean secure = target.getSchemeName().equalsIgnoreCase("https");
                        logger.info("use proxy: {}", thisP);
                        return new HttpRoute(target, null, proxyHost, secure);
                    }
                }

                //insecure proxy
                String dproxy = "127.0.0.1-8888";
                String[] hp = dproxy.split("-");
                HttpHost proxyHost = new HttpHost(hp[0], Integer.parseInt(hp[1]));
                logger.info("use proxy: {}", dproxy);
                return new HttpRoute(host, null, proxyHost, false);
            };
            builder.setRoutePlanner(routePlanner);
            builder.setDefaultCredentialsProvider(new CustomCredentialProvider(userName, passWord));

            asyncClient = builder.build();
            asyncClient.start();
        } catch (Exception e) {
            logger.error("set io reactor error", e);
            System.exit(1);
        }
    }

    public synchronized void importProxy(String proxy) {
        if (proxyCache == null || !StringUtils.equals(proxyCache, proxy)) {
            proxyCache = proxy;

            proxyMap.clear();
            if (StringUtils.isNotBlank(proxy)) {
                String[] ps = proxy.split(";");
                for (String _p : ps) {
                    String[] cp = _p.split(":");
                    String p = cp[0];
                    String[] keys = cp[1].split(",");
                    for (String k : keys) {
                        proxyMap.put(k, p);
                    }
                }
            }
        }

    }

    /**
     * launch http request
     *
     * @param request request
     * @return http response
     */
    public HttpResponse get(final HttpUriRequest request) throws ExecutionException, InterruptedException {
        Future<HttpResponse> responseFuture = asyncClient.execute(request, null);
        return responseFuture.get();
    }

    /**
     * launch http request
     *
     * @return http response
     */
    public void get(String url, BasicHttpClient.Method method) {
        HttpRequestBase httpRequest = null;
        if (BasicHttpClient.Method.post == method) {
            httpRequest = new HttpPost(url);
        } else {
            httpRequest = new HttpGet(url);
        }

        get(httpRequest, response -> {
            HttpEntity entity = response.getEntity();
            String result = EntityUtils.toString(entity);
            System.out.println(result);
        });
    }

    public void get(HttpUriRequest httpRequest, HttpResponseCallBack action) {

        //launch http request
        Future<HttpResponse> responseFuture;
        try {
            // set header
            setHeader(httpRequest);
            responseFuture = asyncClient.execute(httpRequest, null);
            HttpResponse httpResponse = responseFuture.get(60, TimeUnit.SECONDS);

            action.doAction(httpResponse);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * check http response status
     *
     * @param httpResponse http response
     * @param url          url
     */
    private void checkStatus(HttpResponse httpResponse, String url) {
        int code = httpResponse.getStatusLine().getStatusCode();
        //        if (HttpStatus.SC_NOT_FOUND == code) {
        //            throw CustomExceptionBuilder
        //                            .create(Status.SERVICE_UNAVAIL)
        //                            .setDetail(Status.SERVICE_NOT_FOUND)
        //                            .setDetailMessage("404: " + url)
        //                            .addEnv("status", "" + code)
        //                            .build();
        //        } else if (code >= HttpStatus.SC_BAD_REQUEST && code < HttpStatus.SC_INTERNAL_SERVER_ERROR) {
        //            throw CustomExceptionBuilder
        //                            .create(Status.SERVICE_UNAVAIL)
        //                            .setDetail(Status.SERVICE_INNER_UNAVAIL)
        //                            .setDetailMessage("service unavailable: " + url)
        //                            .addEnv("status", "" + code)
        //                            .build();
        //        } else if (code >= HttpStatus.SC_INTERNAL_SERVER_ERROR) {
        //            throw CustomExceptionBuilder
        //                            .create(Status.SERVICE_ERROR)
        //                            .setDetail(Status.SERVICE_INNER_ERROR)
        //                            .setDetailMessage("service error: " + url)
        //                            .addEnv("status", "" + code)
        //                            .build();
        //        }
    }

    //    /**
    //     * set http request header
    //     *
    //     * @param request request
    //     * @param m       model
    //     */
    public static final String USERAGENT_CHROME = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36";

    private void setHeader(HttpUriRequest request) {
        //        request.setHeader("PARAM-TASK-RETRY", "" + times);
        //
        //        if(request.getFirstHeader("PARAM-TASK-ID") == null)
        request.setHeader("PARAM-TASK-ID", "TASKEB000101201708081918390031300000");
        //        if(request.getFirstHeader("PARAM-CHANNEL-TYPE") == null)
        request.setHeader("PARAM-CHANNEL-TYPE", "EB");
        //        if(request.getFirstHeader("PARAM-CHANNEL-CODE") == null)
        request.setHeader("PARAM-CHANNEL-CODE", "000101");
        //        if(request.getFirstHeader("PARAM-REQUEST-TIMEOUT") == null)
        request.setHeader("PARAM-REQUEST-TIMEOUT", "" + 60);
        //
        //        if(request.getHeaders("Accept") == null || request.getHeaders("Accept").length == 0)
        request.setHeader("Accept", "*/*");
        //
        //        if (request.getHeaders("User-Agent") == null || request.getHeaders("User-Agent").length == 0)
        //                request.setHeader("User-Agent", Constants.Agent.USERAGENT_CHROME);
        //
        request.setHeader("User-Agent", USERAGENT_CHROME);
        //        try {
        //            List<HttpCookieEx> cookieList = m.getCookieList();
        //            if(cookieList != null && cookieList.size() > 0)
        //                CookieHelper.setCookies(request.getURI(), request, cookieList);
        //        } catch (URISyntaxException e) {
        //            throw new RuntimeException(e); // never happen
        //        }
    }
}
